Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 8bb38d3e516cac4240b15bd3cc881d1e55daacbb


Parents : 2bef49d
Author : Sudo-Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-01-05T20:21:31-06:00

feat(maintenance): add API endpoint to clear LXMF icons and enhance backup configuration options

Changes
Diff

diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index ed270405..f3c98c78 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -4038,6 +4038,12 @@ class ReticulumMeshChat:
self.database.misc.delete_archived_pages()
return web.json_response({"message": "All archived pages cleared"})
+ # maintenance - clear LXMF icons
+ @routes.delete("/api/v1/maintenance/lxmf-icons")
+ async def maintenance_clear_lxmf_icons(request):
+ self.database.misc.delete_all_user_icons()
+ return web.json_response({"message": "All LXMF icons cleared"})
+
# maintenance - export messages
@routes.get("/api/v1/maintenance/messages/export")
async def maintenance_export_messages(request):
@@ -8519,6 +8525,14 @@ class ReticulumMeshChat:
int(data["archives_max_storage_gb"]),
)
+ if "backup_max_count" in data:
+ try:
+ value = int(data["backup_max_count"])
+ except (TypeError, ValueError):
+ value = self.config.backup_max_count.default_value
+ value = max(1, min(value, 50))
+ self.config.backup_max_count.set(value)
+
# update crawler settings
if "crawler_enabled" in data:
self.config.crawler_enabled.set(self._parse_bool(data["crawler_enabled"]))
@@ -8591,6 +8605,14 @@ class ReticulumMeshChat:
if "message_font_size" in data:
self.config.message_font_size.set(int(data["message_font_size"]))
+ if "message_icon_size" in data:
+ try:
+ value = int(data["message_icon_size"])
+ except (TypeError, ValueError):
+ value = self.config.message_icon_size.default_value
+ value = max(12, min(value, 96))
+ self.config.message_icon_size.set(value)
+
# update desktop settings
if "desktop_open_calls_in_separate_window" in data:
self.config.desktop_open_calls_in_separate_window.set(
@@ -9534,6 +9556,7 @@ class ReticulumMeshChat:
"page_archiver_enabled": ctx.config.page_archiver_enabled.get(),
"page_archiver_max_versions": ctx.config.page_archiver_max_versions.get(),
"archives_max_storage_gb": ctx.config.archives_max_storage_gb.get(),
+ "backup_max_count": ctx.config.backup_max_count.get(),
"crawler_enabled": ctx.config.crawler_enabled.get(),
"crawler_max_retries": ctx.config.crawler_max_retries.get(),
"crawler_retry_delay_seconds": ctx.config.crawler_retry_delay_seconds.get(),
@@ -9569,6 +9592,7 @@ class ReticulumMeshChat:
"banished_text": ctx.config.banished_text.get(),
"banished_color": ctx.config.banished_color.get(),
"message_font_size": ctx.config.message_font_size.get(),
+ "message_icon_size": ctx.config.message_icon_size.get(),
"translator_enabled": ctx.config.translator_enabled.get(),
"libretranslate_url": ctx.config.libretranslate_url.get(),
"desktop_open_calls_in_separate_window": ctx.config.desktop_open_calls_in_separate_window.get(),
@@ -10011,13 +10035,50 @@ class ReticulumMeshChat:
icon_name = icon_appearance[0]
foreground_colour = "#" + icon_appearance[1].hex()
background_colour = "#" + icon_appearance[2].hex()
- self.update_lxmf_user_icon(
- lxmf_message.source_hash.hex(),
- icon_name,
- foreground_colour,
- background_colour,
- context=ctx,
+
+ local_hash = (
+ ctx.local_lxmf_destination.hexhash
+ if ctx.local_lxmf_destination
+ else None
)
+ source_hash = lxmf_message.source_hash.hex()
+
+ # ignore our own icon and empty payloads to avoid overwriting peers with our appearance
+ if source_hash and local_hash and source_hash == local_hash:
+ pass
+ elif (
+ not icon_name or not foreground_colour or not background_colour
+ ):
+ pass
+ else:
+ local_icon_name = ctx.config.lxmf_user_icon_name.get()
+ local_icon_fg = (
+ ctx.config.lxmf_user_icon_foreground_colour.get()
+ )
+ local_icon_bg = (
+ ctx.config.lxmf_user_icon_background_colour.get()
+ )
+
+ # if incoming icon matches our own, skip storing and clear any mistaken stored copy
+ # for now, but this will need to be updated later if two users do have the same icon
+ # FIXME
+ if (
+ local_icon_name
+ and local_icon_fg
+ and local_icon_bg
+ and icon_name == local_icon_name
+ and foreground_colour == local_icon_fg
+ and background_colour == local_icon_bg
+ ):
+ ctx.database.misc.delete_user_icon(source_hash)
+ else:
+ self.update_lxmf_user_icon(
+ source_hash,
+ icon_name,
+ foreground_colour,
+ background_colour,
+ context=ctx,
+ )
except Exception as e:
print("failed to update lxmf user icon from lxmf message")
print(e)

diff --git a/meshchatx/src/backend/bot_handler.py b/meshchatx/src/backend/bot_handler.py
index e5b99a19..ee1baf0c 100644
--- a/meshchatx/src/backend/bot_handler.py
+++ b/meshchatx/src/backend/bot_handler.py
@@ -80,30 +80,66 @@ class BotHandler:
logger.warning("Failed to restore bot %s: %s", entry.get("id"), exc)
def get_status(self):
- bots = []
- for bot_id, bot_info in self.running_bots.items():
- instance = bot_info["instance"]
+ bots: list[dict] = []
+
+ for entry in self.bots_state:
+ bot_id = entry.get("id")
+ template = entry.get("template_id") or entry.get("template")
+ name = entry.get("name") or "Unknown"
+ pid = entry.get("pid")
+
+ running = False
+ if bot_id in self.running_bots:
+ running = True
+ elif pid:
+ running = self._is_pid_alive(pid)
+
+ address_pretty = None
+ address_full = None
+
+ # Try running instance first
+ instance = self.running_bots.get(bot_id, {}).get("instance")
+ if (
+ instance
+ and getattr(instance, "bot", None)
+ and getattr(instance.bot, "local", None)
+ ):
+ try:
+ address_pretty = RNS.prettyhexrep(instance.bot.local.hash)
+ address_full = RNS.hexrep(instance.bot.local.hash, delimit=False)
+ except Exception:
+ pass
+
+ # Fallback to identity file on disk
+ if address_full is None:
+ identity = self._load_identity_for_bot(bot_id)
+ if identity:
+ try:
+ destination = RNS.Destination(identity, "lxmf", "delivery")
+ address_full = destination.hash.hex()
+ address_pretty = RNS.prettyhexrep(destination.hash)
+ except Exception:
+ pass
+
bots.append(
{
"id": bot_id,
- "template": bot_info["template"],
- "name": instance.bot.config.name
- if instance and instance.bot
- else "Unknown",
- "address": RNS.prettyhexrep(instance.bot.local.hash)
- if instance and instance.bot and instance.bot.local
- else "Unknown",
- "full_address": RNS.hexrep(instance.bot.local.hash, delimit=False)
- if instance and instance.bot and instance.bot.local
- else None,
+ "template": template,
+ "template_id": template,
+ "name": name,
+ "address": address_pretty or "Unknown",
+ "full_address": address_full,
+ "running": running,
+ "pid": pid,
+ "storage_dir": entry.get("storage_dir"),
},
)
return {
"has_lxmfy": True,
"detection_error": None,
- "running_bots": bots,
- "bots": self.bots_state,
+ "running_bots": [b for b in bots if b["running"]],
+ "bots": bots,
}
def start_bot(self, template_id, name=None, bot_id=None, storage_dir=None):
@@ -274,8 +310,32 @@ class BotHandler:
if os.path.exists(id_path_alt):
return id_path_alt
+ # LXMFy may nest inside config/lxmf
+ id_path_lxmf = os.path.join(storage_dir, "config", "lxmf", "identity")
+ if os.path.exists(id_path_lxmf):
+ return id_path_lxmf
+
return None
+ def _load_identity_for_bot(self, bot_id):
+ identity_path = self.get_bot_identity_path(bot_id)
+ if not identity_path:
+ return None
+ try:
+ return RNS.Identity.from_file(identity_path)
+ except Exception:
+ return None
+
+ @staticmethod
+ def _is_pid_alive(pid):
+ if not pid:
+ return False
+ try:
+ os.kill(pid, 0)
+ return True
+ except OSError:
+ return False
+
def stop_all(self):
for bot_id in list(self.running_bots.keys()):
self.stop_bot(bot_id)

diff --git a/meshchatx/src/backend/config_manager.py b/meshchatx/src/backend/config_manager.py
index cb4593f9..b5803f3a 100644
--- a/meshchatx/src/backend/config_manager.py
+++ b/meshchatx/src/backend/config_manager.py
@@ -270,6 +270,7 @@ class ConfigManager:
"#dc2626",
)
self.message_font_size = self.IntConfig(self, "message_font_size", 14)
+ self.message_icon_size = self.IntConfig(self, "message_icon_size", 28)
# blackhole integration config
self.blackhole_integration_enabled = self.BoolConfig(

diff --git a/meshchatx/src/backend/database/misc.py b/meshchatx/src/backend/database/misc.py
index 61f40086..9bc603b1 100644
--- a/meshchatx/src/backend/database/misc.py
+++ b/meshchatx/src/backend/database/misc.py
@@ -99,6 +99,15 @@ class MiscDAO:
tuple(destination_hashes),
)
+ def delete_user_icon(self, destination_hash):
+ self.provider.execute(
+ "DELETE FROM lxmf_user_icons WHERE destination_hash = ?",
+ (destination_hash,),
+ )
+
+ def delete_all_user_icons(self):
+ self.provider.execute("DELETE FROM lxmf_user_icons")
+
# Forwarding Rules
def get_forwarding_rules(self, identity_hash=None, active_only=False):
query = "SELECT * FROM lxmf_forwarding_rules WHERE 1=1"

diff --git a/meshchatx/src/backend/identity_context.py b/meshchatx/src/backend/identity_context.py
index 7e901a30..8d500865 100644
--- a/meshchatx/src/backend/identity_context.py
+++ b/meshchatx/src/backend/identity_context.py
@@ -206,6 +206,16 @@ class IdentityContext:
lambda msg: self.app.on_lxmf_delivery(msg, context=self),
)
+ # Restore preferred propagation node on startup
+ try:
+ preferred_node = (
+ self.config.lxmf_preferred_propagation_node_destination_hash.get()
+ )
+ if preferred_node:
+ self.app.set_active_propagation_node(preferred_node, context=self)
+ except Exception:
+ pass
+
# 5. Initialize Handlers and Managers
self.rncp_handler = RNCPHandler(
reticulum_instance=getattr(self.app, "reticulum", None),


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────